/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package estg.dae.projecto.bank.controllers;

import estg.dae.projecto.bank.OperationType;
import estg.dae.projecto.bank.data.AccountDaoLocal;
import estg.dae.projecto.bank.data.AccountMovementDaoLocal;
import estg.dae.projecto.bank.data.ElectronicTransferDaoLocal;
import estg.dae.projecto.bank.impl.ElectronicOperationTO;
import estg.dae.projecto.bank.impl.NibValidator;
import estg.dae.projecto.bank.model.Account;
import estg.dae.projecto.bank.model.AccountMovement;
import estg.dae.projecto.bank.model.BankSettings;
import estg.dae.projecto.bank.model.Client;
import estg.dae.projecto.bank.model.ElectronicTransfer;
import estg.dae.projecto.exceptions.OfflinetransferFailedException;
import java.util.Calendar;
import java.util.Collection;
import javax.ejb.EJB;
import javax.ejb.Stateless;

/**
 *
 * @author Marco
 */
@Stateless
public class AccountControllerBean implements AccountControllerLocal {
    @EJB
    private AccountDaoLocal accountDao;
    @EJB
    private AccountMovementDaoLocal accountMovDao;
    @EJB
    private ElectronicTransferDaoLocal electronicTransferDao;

    public boolean existsNib(String nib) {
        if (!NibValidator.validateNib(nib))
            return false;
        
        return accountDao.existsNib(nib);
    }

    public boolean existsNumber(long number) {
        return accountDao.existsNumber(number);
    }

    public Account get(long id) {
        return accountDao.get(id);
    }

    public Account getByNib(String nib) {
        return accountDao.getByNib(nib);
    }

    public Account getByNumber(long number) {
        return accountDao.getByNumber(number);
    }

    public void save(Account account) {
        if (!NibValidator.validateNib(account.getNib())) return;
        accountDao.save(account);
    }

    public void update(Account account) {
        if (!existsNib(account.getNib())) return;
        accountDao.update(account);
    }

    public void delete(Account account) {
        accountDao.delete(account);
    }

    public Collection<Account> getAll() {
        return accountDao.getAll();
    }

    public Collection<AccountMovement> getMovements(long accountId, int movs) {
        Account account = accountDao.get(accountId);
        return account.getMovements();
        //TODO: resolver problema da leitura de movimentos

        //return accountMovDao.getMovements(account, movs);
    }

    public Collection<ElectronicTransfer> getofflineTransfers() {
        return electronicTransferDao.getAll();
    }

    private ElectronicTransfer getOfflineTransfer(ElectronicOperationTO operation) {
        if (!electronicTransferDao.exists(operation.getOperationType()
                , operation.getOriginNib()
                , operation.getDestinNib()
                , operation.getDate()
                , operation.getValue()))
            return null;

        //se existir devolve a transferência
        return electronicTransferDao.get(operation.getOperationType()
                , operation.getOriginNib()
                , operation.getDestinNib()
                , operation.getDate()
                , operation.getValue());
    }
    public Account createAccount(Client client, int agencyCode, Double overBalance) {
        long accountNumber = 0;
        int bankCode = Integer.parseInt(BankSettings.getInstance().getCode());
        //vai buscar o numero da ultima conta criada
        accountNumber = accountDao.getNextAccountNumber(1);

        //cria a conta
        Account account = new Account();
        account.setAgency(agencyCode);
        account.setClient(client);
        account.setNumber(accountNumber);
        account.setNib(NibValidator.createNib(bankCode, agencyCode, accountNumber));
        account.setOverBalance(overBalance);
        accountDao.save(account);
        
        return account;
    }

    public AccountMovement doWithdraw(String originAccountRef, double value) throws Exception {
        Account originAccount = validateAccount(originAccountRef, "origem");

        if (originAccount==null)
            throw new Exception("A conta não existe.");
        if (originAccount.getAccountingBalance() + originAccount.getOverBalance() < value)
            throw new Exception("Saldo insuficiente.");

        //actualiza os saldos
        originAccount.setAvailableBalance(originAccount.getAvailableBalance() - value);
        originAccount.setAccountingBalance(originAccount.getAccountingBalance() - value);

        AccountMovement mov = new AccountMovement();
        mov.setAccount(originAccount);
        mov.setDescription("Levantamento");
        mov.setOperationDate(Calendar.getInstance().getTime());
        mov.setEfectiveDate(Calendar.getInstance().getTime());
        mov.setMovValue(value);
        mov.setBalance(originAccount.getAvailableBalance());

        accountDao.save(originAccount);
        accountMovDao.save(mov);

        return mov;
    }

    public AccountMovement doTransfer(String originAccountRef, String destinAccountRef, double value) throws Exception {
        Account originAccount = validateAccount(originAccountRef, "origem");
        Account destinAccount = validateAccount(destinAccountRef, "destino");

        if (originAccount==null)
            throw new Exception("A conta de origem não existe.");
        if (originAccount.getAccountingBalance() + originAccount.getOverBalance() < value)
            throw new Exception("Saldo insuficiente.");

        //actualiza os saldos
        originAccount.setAvailableBalance(originAccount.getAvailableBalance() - value);
        originAccount.setAccountingBalance(originAccount.getAccountingBalance() - value);

        AccountMovement mov = new AccountMovement();
        mov.setAccount(originAccount);
        mov.setDescription("Transferência");
        mov.setOperationDate(Calendar.getInstance().getTime());
        if (destinAccount!=null)
            mov.setEfectiveDate(Calendar.getInstance().getTime());
        mov.setMovValue(value);
        mov.setBalance(originAccount.getAvailableBalance());

        accountDao.update(originAccount);
        accountMovDao.save(mov);

        if (destinAccount==null) {
            //faz uma transferência offline
            ElectronicTransfer transfer = new ElectronicTransfer();
            transfer.setOriginNib(originAccount.getNib());
            transfer.setDestinNib(destinAccountRef);
            transfer.setMovement(mov);
            transfer.setOperationType(OperationType.Transfer);
            transfer.setDate(mov.getOperationDate());
            transfer.setValue(value);
            electronicTransferDao.save(transfer);
        } else {
            //faz uma transferência online

            //actualiza os saldos
            destinAccount.setAvailableBalance(destinAccount.getAvailableBalance() + value);
            destinAccount.setAccountingBalance(destinAccount.getAccountingBalance() + value);

            AccountMovement mov2 = new AccountMovement();
            mov2.setAccount(destinAccount);
            mov2.setDescription("Transferência");
            mov2.setOperationDate(Calendar.getInstance().getTime());
            mov2.setEfectiveDate(Calendar.getInstance().getTime());
            mov2.setMovValue(value);
            mov2.setBalance(destinAccount.getAvailableBalance());

            accountDao.update(destinAccount);
            accountMovDao.save(mov2);
        }

        return mov;
    }

    public AccountMovement doMoneyDeposit(String destinAccountRef, double value) throws Exception {
        Account destinAccount = validateAccount(destinAccountRef, "destino");

        if (destinAccount==null)
            throw new Exception("A conta de origem não existe.");

        //actualiza os saldos
        destinAccount.setAvailableBalance(destinAccount.getAvailableBalance() + value);
        destinAccount.setAccountingBalance(destinAccount.getAccountingBalance() + value);

        AccountMovement mov = new AccountMovement();
        mov.setAccount(destinAccount);
        mov.setDescription("Depósito Numerário");
        mov.setOperationDate(Calendar.getInstance().getTime());
        mov.setEfectiveDate(Calendar.getInstance().getTime());
        mov.setMovValue(value);
        mov.setBalance(destinAccount.getAvailableBalance());

        accountDao.save(destinAccount);
        accountMovDao.save(mov);

        return mov;
    }

    public AccountMovement doCheckDeposit(String originAccountRef, String destinAccountRef, double value) throws Exception {
        Account originAccount = validateAccount(originAccountRef, "origem");
        Account destinAccount = validateAccount(destinAccountRef, "destino");

        if (destinAccount==null)
            throw new Exception("A conta de destino não existe.");
        if (originAccount != null && originAccount.getAccountingBalance() + originAccount.getOverBalance() < value)
            throw new Exception("A conta de origem não tem saldo insuficiente.");

        //actualiza os saldos
        if (originAccount!=null)
            destinAccount.setAvailableBalance(destinAccount.getAvailableBalance() + value);
        destinAccount.setAccountingBalance(destinAccount.getAccountingBalance() + value);

        AccountMovement mov = new AccountMovement();
        mov.setAccount(destinAccount);
        mov.setDescription("Depósito Cheque");
        mov.setOperationDate(Calendar.getInstance().getTime());
        if (originAccount!=null)
            mov.setEfectiveDate(Calendar.getInstance().getTime());
        mov.setMovValue(value);
        mov.setBalance(destinAccount.getAvailableBalance());

        accountDao.update(destinAccount);
        accountMovDao.save(mov);

        if (originAccount==null) {
            //faz depósito de cheque offline
            //faz uma transferência offline
            ElectronicTransfer transfer = new ElectronicTransfer();
            transfer.setOriginNib(originAccountRef);
            transfer.setDestinNib(destinAccount.getNib());
            transfer.setMovement(mov);
            transfer.setOperationType(OperationType.CheckDeposit);
            transfer.setDate(mov.getOperationDate());
            transfer.setValue(value);
            electronicTransferDao.save(transfer);
        } else {
            //faz depósito de cheque online

            //actualiza os saldos
            originAccount.setAvailableBalance(originAccount.getAvailableBalance() - value);
            originAccount.setAccountingBalance(originAccount.getAccountingBalance() - value);

            AccountMovement mov2 = new AccountMovement();
            mov2.setAccount(originAccount);
            mov2.setDescription("Levantamento Cheque");
            mov2.setOperationDate(Calendar.getInstance().getTime());
            mov2.setEfectiveDate(Calendar.getInstance().getTime());
            mov2.setMovValue(value);
            mov2.setBalance(originAccount.getAvailableBalance());

            accountDao.update(originAccount);
            accountMovDao.save(mov2);
        }

        return mov;
    }

    ///Devolve a conta ou null se a conta for externa (de outro banco)
     private Account validateAccount(String accountRef, String message) throws Exception {
        if (accountRef.length() == 21) {
            if (isLocalAccount(accountRef)) {
                //se a conta for local tem de existir
                if (!accountDao.existsNib(accountRef))
                    throw new Exception("A conta de "+message+" não existe");
                
                return accountDao.getByNib(accountRef);
            } else {
                if (!NibValidator.validateNib(accountRef))
                    throw new Exception("O NIB da conta de "+message+" não é válido.");

                return null;
            }
        } else {
            //se for o numero da conta tem de existir
            long accountNumber = Long.parseLong(accountRef);
            if (!accountDao.existsNumber(accountNumber)) {
                throw new Exception("A conta de "+message+" não existe");
            }
            return accountDao.getByNumber(accountNumber);
        }
    }

    private boolean isLocalAccount(String accountRef) {
        BankSettings bankSettings = BankSettings.getInstance();
        return bankSettings.getCode().equals(accountRef.substring(0,4));
    }

    public void executeOnlineOperation(ElectronicOperationTO operation) throws OfflinetransferFailedException {
        if (operation.getOperationType() == OperationType.Transfer) {
            //Se for uma transferência
            //envia o valor para o banco destino
            if (!accountDao.existsNib(operation.getDestinNib()))
                throw new OfflinetransferFailedException("A conta de destino não existe.");
            
            Account account = accountDao.getByNib(operation.getDestinNib());

            account.setAccountingBalance(account.getAccountingBalance() + operation.getValue());
            account.setAvailableBalance(account.getAvailableBalance() + operation.getValue());

            AccountMovement mov = new AccountMovement();
            mov.setAccount(account);
            mov.setOperationDate(Calendar.getInstance().getTime());
            mov.setEfectiveDate(Calendar.getInstance().getTime());
            mov.setMovValue(operation.getValue());
            mov.setBalance(account.getAvailableBalance());
            mov.setDescription("Transferência Interbancária ("+ operation.getOriginNib()+")");

            accountDao.update(account);
            accountMovDao.save(mov);

        } else if (operation.getOperationType() == OperationType.CheckDeposit) {
            if (!accountDao.existsNib(operation.getOriginNib()))
                throw new OfflinetransferFailedException("A conta de origem não existe.");

            Account account = accountDao.getByNib(operation.getOriginNib());

            if (account.getAvailableBalance() + account.getOverBalance() < operation.getValue())
                throw new OfflinetransferFailedException("A conta de origem não tem saldo suficiente.");

            account.setAccountingBalance(account.getAccountingBalance() - operation.getValue());
            account.setAvailableBalance(account.getAvailableBalance() - operation.getValue());

            AccountMovement mov = new AccountMovement();
            mov.setAccount(account);
            mov.setOperationDate(Calendar.getInstance().getTime());
            mov.setEfectiveDate(Calendar.getInstance().getTime());
            mov.setMovValue(operation.getValue());
            mov.setBalance(account.getAvailableBalance());
            mov.setDescription("Depósito Interbancário ("+ operation.getDestinNib()+")");

            accountDao.update(account);
            accountMovDao.save(mov);
        }
    }
    public void confirmOnlineOperation(ElectronicOperationTO operation) throws OfflinetransferFailedException {
        ElectronicTransfer transfer = getOfflineTransfer(operation);
        
        if (transfer == null) {
            throw new OfflinetransferFailedException("A transferência já não existe.");
        }

        if (transfer.getOperationType() == OperationType.Transfer) {
            //Se for uma transferência
            //envia o valor para o banco destino
            electronicTransferDao.delete(transfer);
        } else if (transfer.getOperationType() == OperationType.CheckDeposit) {
            //Se for um depósito em cheque
            //envia retira o valor do banco origem

            AccountMovement mov = transfer.getMovement();
            Account account = mov.getAccount();

            account.setAvailableBalance(account.getAvailableBalance() + transfer.getValue());
            mov.setBalance(account.getAvailableBalance());
            mov.setEfectiveDate(Calendar.getInstance().getTime());
            accountDao.update(account);
            accountMovDao.update(mov);

            electronicTransferDao.delete(transfer);
        } else {
            throw new OfflinetransferFailedException("Operação inválida.");
        }
    }
    public void failedOnlineOperation(ElectronicOperationTO operation, String message) {
        ElectronicTransfer transfer = getOfflineTransfer(operation);

        //se a transferencia já não existe ignora a chamda
        if (transfer == null)
            return;

        if (transfer.getOperationType() == OperationType.Transfer) {
            //Se for uma transferência
            AccountMovement mov = transfer.getMovement();
            Account account = mov.getAccount();

            mov.setEfectiveDate(Calendar.getInstance().getTime());

            //repõe o dinheiro da transferencia
            account.setAvailableBalance(account.getAvailableBalance() + transfer.getValue());
            account.setAccountingBalance(account.getAccountingBalance() + transfer.getValue());

            //efectua o movimento de reposição com a mesma data de realização do movimento original
            AccountMovement mov2 = new AccountMovement();
            mov2.setAccount(account);
            mov2.setOperationDate(mov.getEfectiveDate());
            mov2.setEfectiveDate(mov.getEfectiveDate());
            mov2.setMovValue(operation.getValue());
            mov2.setBalance(account.getAvailableBalance());
            mov2.setDescription("Cancelamento de Transferência ("+message+")");
            accountDao.update(account);
            accountMovDao.update(mov);
            accountMovDao.save(mov2);

            //e remove a transferência da lista
            electronicTransferDao.delete(transfer);
        } else if (transfer.getOperationType() == OperationType.CheckDeposit) {
            //Se for um depósito em cheque
            AccountMovement mov = transfer.getMovement();
            Account account = mov.getAccount();

            mov.setEfectiveDate(Calendar.getInstance().getTime());

            //repõe o o saldo contabilistico
            account.setAccountingBalance(account.getAccountingBalance() - transfer.getValue());

            //efectua o movimento de cancelamento da transferencia
            //com a mesma data de realização do movimento original
            AccountMovement mov2 = new AccountMovement();
            mov2.setAccount(account);
            mov2.setOperationDate(mov.getEfectiveDate());
            mov2.setEfectiveDate(mov.getEfectiveDate());
            mov2.setMovValue(operation.getValue());
            mov2.setBalance(account.getAvailableBalance());
            mov2.setDescription("Cancelamento de Depósito Chq. ("+message+")");
            accountDao.update(account);
            accountMovDao.update(mov);
            accountMovDao.save(mov2);

            //e remove a transferência da lista
            electronicTransferDao.delete(transfer);
        }
    }
}
